
bl_info = {
    "name": "Export OSSL Animation File",
    "author": "Tom Ernst",
    "version": (1, 0, 0),
    "blender": (2, 93, 0),
    "location": "Topbar > OSSL Menu > Export OSSL Animation File",
    "description": "Export OSSL Animation File from Armature Object",
    "warning": "",
    "doc_url": "https://rowlplay.net/blender/animexp.txt",
    "category": "Import-Export",
}

import bpy, os
from math import pi
from struct import pack, unpack
from mathutils import Vector, Matrix, Euler

class Format_ANIM(object):
    def __init__(self, anim, priority, fps, frames, loop, hand_pose, ease_duration):
        self.anim = anim
        self.outfile = None
        self.priority = priority
        self.fps = fps
        self.frames = frames
#        print("\nframes= ", frames)
#        for i in frames: print("\nframe ", i)
#        print("\n")
        self.hand_pose = hand_pose
        self.ease_duration = ease_duration

        self.frame_count = frames.stop - frames.start
        self.duration = self.frame_count * (1.0 / fps)
        
        if len(loop) == 0:
            self.loop = None
            self.loop_in_point = 0.0
            self.loop_out_point = 0.0
        else:
            self.loop = loop
            self.loop_in_point = loop.start / fps
            self.loop_out_point = loop.stop / fps

    def export(self, filepath):
        print(self.anim, "exporting to", filepath)
        try: os.remove(filepath)
        except: pass
        self.outfile = open(filepath, 'wb')
        try:
            self.write_anim_file_header()
            self.write_anim_file_bones()
            self.write_anim_file_constraints()
        finally:
            self.outfile.close()

        print("ANIM Exported: %s frames:%d\n" % (filepath, self.frame_count))
        return {'FINISHED'}

    def tell(self):
        return "%06X"%self.outfile.tell()
        
    def write_anim_file_header(self):
        off = self.tell()
        version = (1, 0)
        self.pack_tuple("HH", version)
        print("\n" + off, "version (main,sub): ", version)
        
        off = self.tell()
        priority = self.priority
        self.pack_scalar("I", priority)
        print(off, "priority: ", priority)
        
        off = self.tell()
        duration = self.duration
        self.pack_scalar("f", duration)
        print(off, "duration: ", duration)
        
        off = self.tell()
        emote_name = ""
        self.pack_string_zt(emote_name)
        print(off, "emote_name: ", emote_name)
        
        off = self.tell()
        loop_range = (self.loop_in_point, self.loop_out_point)
        self.pack_tuple("ff", loop_range)
        print(off, "loop_range (in,out): ", loop_range)

        off = self.tell()
        looped = self.loop != None
        self.pack_scalar("I", looped)
        print(off, "looped: ", looped)

        off = self.tell()
        ease_duration = self.ease_duration
        self.pack_tuple("ff", ease_duration)
        print(off, "ease_duration (in,out): ", ease_duration)

        off = self.tell()
        hand_pose = self.hand_pose
        self.pack_scalar("I", hand_pose)
        print(off, "hand_pose: ", hand_pose)

    def recursive_used(self, bone):
        n = 1 if bone.use_loc or bone.use_rot else 0
        for b in bone.children: n += self.recursive_used(b)
        return n

    def write_anim_file_bones(self):
        num_joints = 0
        for bone in self.anim.armature.root_bones: num_joints += self.recursive_used(bone)

        off = self.tell()
        #num_joints = len(bone_dict)
        self.pack_scalar("I", num_joints)
        print("\n" + off, "num_joints: ", num_joints)

        for bone in self.anim.armature.root_bones: self.recursive_write_bone_frames(bone)

    def write_anim_file_constraints(self):
        off = self.tell()
        num_constraints = 0
        self.pack_scalar("I", num_constraints)
        print("\n" + off, "num_constraints: ", num_constraints)
        for constraint in []: self.write_constraint(constraint)

    def recursive_write_bone_frames(self, bone):
        def diff0(v1, v2):
            if type(v1) is Vector:
                for i in range(0, len(v1)):
                    if diff0(v1[i], v2[i]): return True
                return False
            return abs(v1 - v2) >= 0.0000001
        def diff(v1, v2):
            for i in range(0, len(v1)):
                if diff0(v1[i], v2[i]): return True
            return False
        def append_if_diff(frame, vals, val):
            if frame == 0: vals.append([frame, frame, val])
            else:
                if diff(vals[-1][2], val): vals.append([frame, frame, val])
                else:
                    vals[-1][1] = frame

        if bone.use_rot or bone.use_loc:
            rots = []
            locs = []
            for frame in self.frames:
                loc, rot = bone.frame_values[frame]
                append_if_diff(frame, rots, rot)
                append_if_diff(frame, locs, loc)

            off = self.tell()
            self.pack_string_zt(bone.name)
            print("\n" + off, "\tjoint_name: ", bone.name)
            
            off = self.tell()
            priority = self.priority
            self.pack_scalar("I", priority)
            print(off, "\t\tjoint_priority: ", priority)
            
            for parm in ("rot", "loc"):
                if eval("bone.use_" + parm):
                    bone_frames = eval(parm + "s")
                else: bone_frames = []
                
                fn = self.map_F2R if parm == "rot" else self.map_F2P

                off = self.tell()
                num_keyframes = len(bone_frames)
                for i in range(0, len(bone_frames)):
                    if bone_frames[i][1] > bone_frames[i][0]: num_keyframes += 1
                self.pack_scalar("I", num_keyframes)
                print("\n" + off, "\t\tnum_" + parm + "_keyframes: ", num_keyframes)
                
                for i in range(0, len(bone_frames)):                
                    def po(t, val):
                        off = self.tell()
                        time = self.map_I2T(float(t))
                        self.pack_scalar("H", time[2])
                        print("\n" + off, "\t\t\ttime: ", time)
                        
                        off = self.tell()
                        keyframe = fn(val)
                        self.pack_tuple("HHH", keyframe)
                        if type(val) is Matrix: val = val.to_quaternion()
                        print(off, "\t\t\t" + parm + "_keyframe (x,y,z): ", val, keyframe)

                    vals = bone_frames[i]
                    po(vals[0], vals[2])
                    if vals[0] != vals[1]: po(vals[1], vals[2])
            
        for b in bone.children: self.recursive_write_bone_frames(b)

    def write_constraint(self, constraint):
        off = self.tell()
        chain_length = 0
        self.pack_scalar("H", chain_length)
        print("\n" + off, "\tchain_length: ", chain_length)
        
        off = self.tell()
        constraint_type = 0
        self.pack_scalar("H", constraint_type)
        print(off, "\tconstraint_type: ", constraint_type)
        
        off = self.tell()
        source_volume = ""
        self.pack_string_lzt(16, source_volume)
        print(off, "\tsource_volume: ", source_volume)
        
        off = self.tell()
        source_offset = (0,0,0)
        self.pack_tuple("fff", source_offset)
        print(off, "\tsource_offset (x,y,z): ", source_offset)
        
        off = self.tell()
        target_volume = ""
        self.pack_string_lzt(16, target_volume)
        print(off, "\ttarget_volume: ", target_volume)
        
        off = self.tell()
        target_offset = (0,0,0)
        self.pack_tuple("fff", target_offset)
        print(off, "\ttarget_offset (x,y,z): ", target_offset)
        
        off = self.tell()
        target_direction = (0,0,0,0)
        self.pack_tuple("fff", target_direction)
        print(off, "\ttarget_direction (x,y,z): ", target_direction)
        
        off = self.tell()
        ease_in = (0,0)
        self.pack_tuple("ff", ease_in)
        print(off, "\tease_in (start,stop): ", ease_in)
        
        off = self.tell()
        ease_out = (0,0)
        self.pack_tuple("ff", ease_out)
        print(off, "\tease_out(start,stop): ", ease_out)

        
    def pack_scalar(self, fmt, val):
        self.outfile.write(pack(fmt, val))
    
    def pack_tuple(self, fmt, val):
        self.outfile.write(pack(fmt, *val[0:len(fmt)]))
    
    def pack_string_zt(self, s):
        self.outfile.write((s + "\x00").encode("UTF-8"))
    
    def pack_string_lzt(self, ln, s):
        self.outfile.write((s.encode("UTF-8")+bytes(ln))[0:ln])
    
    def B2OSSL(self, val, vmin, vmax):
        return min(65535, max(0, int(65535.0 * (val - vmin) / (vmax-vmin))))

    def map_I2T(self, f):
        ft = f / self.fps
        return [int(f), ft, self.B2OSSL(ft, 0, self.duration)]

    def map_F2P(self, v):
        v = (v[2],v[0],v[1])
        return tuple(self.B2OSSL(x, -5.0, 5.0) for x in v)
    
    def map_F2R(self, v):
        v = v.to_euler()
        v = Euler((-v[2],-v[0],-v[1]))
        v = v.to_quaternion()[1:]
        return tuple(self.B2OSSL(x, -1, 1) for x in v)
    
    def rd(self, a):
        if a is None: return None
        return tuple(round(x,4) for x in a)


    def import_(self, filepath):
        print(self.anim, "importing from", filepath)
        self.outfile = open(filepath, 'rb')
        try:
            self.read_anim_file_header()
            self.read_anim_file_bones()
            self.read_anim_file_constraints()
        finally:
            self.outfile.close()

        print("ANIM Imported: %s frames:%d\n" % (filepath, self.frame_count))
        return {'FINISHED'}

    def read_anim_file_header(self):
        off = self.tell()
        version = self.unpack_tuple("HH")
        print("\n" + off, "version (main,sub): ", version)
        
        off = self.tell()
        priority = self.priority
        priority = self.unpack_scalar("I", priority)
        print(off, "priority: ", priority)
        
        off = self.tell()
        duration = self.unpack_scalar("f")
        print(off, "duration: ", duration)
        
        off = self.tell()
        emote_name = self.unpack_string_zt()
        print(off, "emote_name: ", emote_name)
        
        off = self.tell()
        loop_range = (self.loop_in_point, self.loop_out_point)
        loop_range = self.unpack_tuple("ff")
        print(off, "loop_range (in,out): ", loop_range)

        off = self.tell()
        looped = self.unpack_scalar("I")
        print(off, "looped: ", looped)

        off = self.tell()
        ease_duration = self.unpack_tuple("ff")
        print(off, "ease_duration (in,out): ", ease_duration)

        off = self.tell()
        hand_pose = self.hand_pose
        hand_pose = self.unpack_scalar("I")
        print(off, "hand_pose: ", hand_pose)

    def read_anim_file_bones(self):
        num_joints = 0
        for bone in self.anim.armature.root_bones: num_joints += self.recursive_used(bone)

        off = self.tell()
        #num_joints = len(bone_dict)
        num_joints = self.unpack_scalar("I")
        print("\n" + off, "num_joints: ", num_joints)

        for bone in self.anim.armature.root_bones: self.recursive_read_bone_frames(bone)

    def read_anim_file_constraints(self):
        off = self.tell()
        num_constraints = 0
        num_constraints = self.unpack_scalar("I")
        print("\n" + off, "num_constraints: ", num_constraints)
        for constraint in []: self.read_constraint(constraint)

    def lng(fmt):
        return 2

    def unpack_scalar(self, fmt):
        return unpack(fmt, self.outfile.read(self.lng(fmt)))
    
    def unpack_tuple(self, fmt, val):
        return unpack(fmt, self.outfile.read(self.lng(fmt)))
        #self.outfile.write(pack(fmt, *val[0:len(fmt)]))

    def unpack_string_zt(self, s):
        return self.outfile.read(10)
        #self.outfile.write((s + "\x00").encode("UTF-8"))
    
    def unpack_string_lzt(self, ln, s):
        return self.outfile.read(10)
        #self.outfile.write((s.encode("UTF-8")+bytes(ln))[0:ln])
    
    def OSSL2B(self, val, vmin, vmax):
        return min(65535, max(0, int(65535.0 * (val - vmin) / (vmax-vmin))))

    def map_T2I(self, f):
        ft = f / self.fps
        return [int(f), ft, self.OSSL2B(ft, 0, self.duration)]

    def map_P2F(self, v):
        v = (v[2],v[0],v[1])
        return tuple(self.OSSL2B(x, -5.0, 5.0) for x in v)
    
    def map_R2F(self, v):
        v = v.to_euler()
        v = Euler((-v[2],-v[0],-v[1]))
        v = v.to_quaternion()[1:]
        return tuple(self.OSSL2B(x, -1, 1) for x in v)

 
def register():
    print("registering: OSSL Format ANIM")
    print("registered: OSSL Format ANIM")

def unregister():
    print("unregistering: OSSL Format ANIM")
    print("unregistered: OSSL Format ANIM")

import ossl.util as util

if __name__ == "__main__":
    util.reset_addon()
